// This is a header only library.
//
// How to use for maintainers.
// 1. Put "noscript.hpp" to root of source tree.
// 2. Add "noscript.cpp" to include "noscript.hpp" with "NOSCRIPT_IMPL".
// 3. Call "noscript::RebuildNoScript" in main function.
// 4. Call other functions to build the project after.
//
// How to use for developers.
// 1. Build "noscript.cpp" to "noscript" with a C++ compiler.
// 2. Run ./noscript to build the project.

#ifndef NOSCRIPT_HPP_
#define NOSCRIPT_HPP_

#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// C++
#include <cassert>
#include <cerrno>
#include <cstdint>
#include <string>
#include <vector>

// POSIX
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>

#include <cstdarg>
#include <ctime>

namespace noscript {

// ----------------------------------------------------------------------------
// Base
// ----------------------------------------------------------------------------

#define NOSCRIPT_IS_ON_concat(X, Y) X##Y
#define NOSCRIPT_IS_ON(X) NOSCRIPT_IS_ON_concat(0, X)

// Open debug options, more logs and more checks.
#ifndef NOSCRIPT_DEBUG
#define NOSCRIPT_DEBUG 0
#endif

typedef unsigned long usize;
typedef long isize;

enum Error {
    ERR_OK = 0,
    ERR_OOPS,
    ERR_CHILD,
};

class Slice {
public:
    Slice(const char* start, size_t len)
        : start_(start)
        , len_(len)
    {
    }

    std::string ToString() const { return std::string(start_, len_); }
    void AppendTo(std::string* string) const { string->append(start_, len_); }

    char const* GetStart() const { return start_; }
    size_t GetLength() const { return len_; }

private:
    char const* start_;
    size_t len_;
};

class StringTokenizer {
public:
    StringTokenizer(const char* text)
        : text_(text)
    {
    }

    // Return Token{NULL, 0} if no more tokens.
    Slice Next(char sep)
    {
        int c = text_[curr_];

        if (c == 0) {
            return Slice { NULL, 0 };
        }

        size_t start = curr_;
        for (; (c = text_[curr_]) && c != sep; ++curr_) {
        }

        Slice token { text_ + start, curr_ - start };
        if (c != 0)
            ++curr_;

        return token;
    }

    size_t GetCurrent() const { return curr_; }

private:
    size_t curr_ { 0 };
    char const* text_;
};

void panic(char const* message);

#define NOSCRIPT_ASSERT(COND)              \
    do {                                   \
        if (COND)                          \
            break;                         \
                                           \
        panic("Asseration error: " #COND); \
    } while (false)

#if NOSCRIPT_IS_ON(NOSCRIPT_DEBUG)
#define NOSCRIPT_DEBUG_ASSERT(COND) NOSCRIPT_ASSERT(COND)
#else
#define NOSCRIPT_DEBUG_ASSERT(COND) ((void)0)
#endif

template <typename T>
struct NoRelease {
    enum { ENABLE = false };
    static void Run(T*) { }
};

template <typename T>
struct ReleaseByFreePtr {
    enum { ENABLE = true };
    static void Run(T* x) { free(*x); }
};

template <typename T>
struct ReleaseByDelete {
    enum { ENABLE = true };
    static void Run(T* x) { x->~T(); }
};

enum LogLevel {
    LOG_DEBUG,
    LOG_INFO,
    LOG_ERROR,
};

constexpr char const* kColorRed = "\e[31m";
constexpr char const* kColorYellow = "\e[33m";
constexpr char const* kColorMagenta = "\e[35m";
constexpr char const* kColorBoldWhite = "\e[1;37m";
constexpr char const* kColorReset = "\e[0m";

extern LogLevel MIN_LOG_LEVEL;

#define NOSCRIPT_LOG_IS_ON(LEVEL) (noscript::LOG_##LEVEL < noscript::MIN_LOG_LEVEL)
#define NOSCRIPT_LOG_CTX(LEVEL) noscript::LogCtx(stderr, __FILE__, __LINE__, noscript::LOG_##LEVEL)

#define NOSCRIPT_LOG(LEVEL, ...)                        \
    do {                                                \
        if (NOSCRIPT_LOG_IS_ON(LEVEL))                  \
            break;                                      \
        noscript::LogCtx ctx = NOSCRIPT_LOG_CTX(LEVEL); \
        ctx.Printf(__VA_ARGS__);                        \
    } while (false)

class Pid {
public:
    pid_t Get() const { return pid_; }
    void Sync() { pid_ = getpid(); }

    static Pid& Instance()
    {
        static Pid pid;
        return pid;
    }

private:
    explicit Pid() { Sync(); }

    pid_t pid_;
};

class LogCtx {
public:
    explicit LogCtx(FILE* fp, char const* file, int line, LogLevel level);

    void Printf(char const* fmt, ...)
    {
        va_list ap;
        va_start(ap, fmt);
        vfprintf(stderr, fmt, ap);
        va_end(ap);
    }

    ~LogCtx()
    {
        fprintf(fp_, "%s\n", kColorReset);
        fflush(fp_);
    }

private:
    FILE* fp_;
};

#define NOSCRIPT_TRY(X)                                 \
    do {                                                \
        noscript::Error noscript_try__ = (X);           \
        if (noscript_try__ == noscript::ERR_OK)         \
            break;                                      \
        NOSCRIPT_LOG(ERROR, "failed to try: `%s'", #X); \
        return noscript_try__;                          \
    } while (false)

#define NOSCRIPT_TRY2(X1, X2) \
    do {                      \
        NOSCRIPT_TRY(X1);     \
        NOSCRIPT_TRY(X2);     \
    } while (false)

#define NOSCRIPT_TRY3(X1, ...)      \
    do {                            \
        NOSCRIPT_TRY(X1);           \
        NOSCRIPT_TRY2(__VA_ARGS__); \
    } while (false)

#define NOSCRIPT_TRY4(X1, ...)      \
    do {                            \
        NOSCRIPT_TRY(X1);           \
        NOSCRIPT_TRY3(__VA_ARGS__); \
    } while (false)

#define NOSCRIPT_TRY5(X1, ...)      \
    do {                            \
        NOSCRIPT_TRY(X1);           \
        NOSCRIPT_TRY4(__VA_ARGS__); \
    } while (false)

// ----------------------------------------------------------------------------
// Path
// ----------------------------------------------------------------------------

static inline Error MkdirAllowExists(std::string const& path)
{
    int c = mkdir(path.c_str(), 0755);

    if (c != 0 && errno != EEXIST) {
        NOSCRIPT_LOG(ERROR, "failed to create dir `%s': %s", path.c_str(), strerror(errno));
        return ERR_OOPS;
    }

    return ERR_OK;
}

// Path to a file in file system.
//
// This is not stored in a string, but a vector of strings. Each component of the path is
// an element of the array.
//
// For example:
//
//   Path({"usr", "lib"});
//   Path() / "usr" / "lib";
class Path {
public:
    explicit Path(std::vector<std::string> components)
        : components_(std::move(components))
    {
    }
    Path() { }

    Path SetExtension(std::string const& name) const;

    Path Join(std::string component) const;

    Path operator/(std::string component) const { return Join(component); }

    Path JoinPath(Path const& path) const;

    Path operator/(Path const& path) const { return JoinPath(path); }

    std::string ToString() const;

    Error Mkdir();
    Path Parent() const;

    Path Clone() const { return *this; }
    void Clear() { components_.clear(); }

private:
    std::vector<std::string> components_;
};

class Path2 {
public:
    Path2(Path2 const* parent, std::string component)
        : parent_(parent)
        , component_(std::move(component))
    {
    }
    explicit Path2(std::string component)
        : parent_(nullptr)
        , component_(std::move(component))
    {
    }

    Path2 Join(std::string component) const { return Path2(this, component); }

    std::string const& GetString() const
    {
        if (string_cache_.empty()) {
            string_cache_ = (parent_ == nullptr) ? "" : parent_->GetString();
            string_cache_.push_back('/');
            string_cache_.append(component_);

            if (parent_ == nullptr) {
                string_cache_ = component_;
            } else {
                string_cache_ = parent_->GetString();
                string_cache_.push_back('/');
                string_cache_.append(component_);
            }
        }

        return string_cache_;
    }

    Error Mkdir() const
    {
        Error err;

        if (parent_ != nullptr) {
            err = parent_->Mkdir();
            if (err != ERR_OK)
                return err;
        }

        return MkdirAllowExists(GetString());
    }

private:
    Path2 const* parent_;
    std::string component_;
    mutable std::string string_cache_ {};
};

// ----------------------------------------------------------------------------
// External command
// ----------------------------------------------------------------------------

class Cmd;

class Child {
public:
    Child(Error err, pid_t pid)
        : err_(err)
        , pid_(pid)
    {
    }

    Error GetError() const { return err_; }
    bool IsOk() const { return err_ == ERR_OK; }

    Error Join();

private:
    Error err_;
    pid_t pid_;
};

typedef std::vector<std::string> StringSeq;

struct ChildOutput {
    ChildOutput(std::string stdout__, std::string stderr__)
        : error(ERR_OK)
        , status(0)
        , child_stdout(std::move(stdout__))
        , child_stderr(std::move(stderr__))
    {
    }

    explicit ChildOutput(Error err__, int status__)
        : error(err__)
        , status(status__)
    {
    }

    Error error;
    int status;
    std::string child_stdout;
    std::string child_stderr;
};

class Cmd {
public:
    explicit Cmd(std::string name) { argv_.push_back(name); }
    explicit Cmd(Path const& path) { argv_.push_back(path.ToString()); }

    Cmd& Arg(std::string arg)
    {
        argv_.push_back(arg);
        return *this;
    }

    Cmd& ArgPath(Path const& path)
    {
        argv_.push_back(path.ToString());
        return *this;
    }

    StringSeq const& GetArgs() const { return argv_; }

    Child Spawn();
    ChildOutput GetOutput();
    Error Run();

    ~Cmd();

private:
    void RunChild();

    StringSeq argv_;
};

class CmdPool {
public:
    explicit CmdPool(usize n)
        : cap_(n)
    {
    }

    Error Spawn(Cmd* cmd)
    {
        Error err = ERR_OK;

        if (cmds_.size() >= cap_) {
            err = Join();
            if (err != ERR_OK)
                return err;
        }

        cmds_.push_back(cmd->Spawn());

        return err;
    }

    Error Join()
    {
        Error err = ERR_OK;

        for (size_t i = 0; i < cmds_.size(); ++i) {
            err = cmds_[i].Join();
            if (err != ERR_OK)
                return err;
        }
        cmds_.clear();

        return err;
    }

private:
    size_t cap_;
    std::vector<Child> cmds_;
};

class Rule {
public:
    virtual Error Build() = 0;
    virtual ~Rule() { }

    void Input(char const* x) { inputs_.push_back(x); }
    void Output(char const* x) { outputs_.push_back(x); }

    StringSeq const& GetInputs() const { return inputs_; }
    StringSeq const& GetOutputs() const { return outputs_; }

private:
    StringSeq inputs_;
    StringSeq outputs_;
};

// ----------------------------------------------------------------------------
// C++
// ----------------------------------------------------------------------------

static inline bool FindExecutableInPath(char const* name)
{
    char const* path_array = getenv("PATH");
    if (path_array == NULL)
        return false;

    StringTokenizer path_tokenizer(path_array);
    Slice token(NULL, 0);
    struct stat st;

    std::string path;
    while (true) {
        token = path_tokenizer.Next(':');
        if (token.GetStart() == NULL)
            break;

        path.clear();
        token.AppendTo(&path);
        path.push_back('/');
        path.append(name);

        int err = stat(path.c_str(), &st);
        if (err != 0) {
            if (errno == ENOENT)
                continue;

            NOSCRIPT_LOG(ERROR, "stat: %s", strerror(errno));
            continue;
        }

        // Is regular file and executable for owner.
        if ((st.st_mode & S_IFMT) == S_IFREG && st.st_mode & S_IXUSR) {
            return true;
        }
    }

    return false;
}

static inline char const* GetCxx()
{
    char const* cxx = getenv("NOSCRIPT_CXX");
    if (cxx != NULL)
        return cxx;

    cxx = getenv("CXX");
    if (cxx != NULL)
        return cxx;

    return "c++";
}

static inline char const* GetCCache()
{
    char const* ccache = getenv("NOSCRIPT_CCACHE");
    if (ccache != NULL)
        return ccache;

    if (FindExecutableInPath("ccache"))
        return "ccache";
    return NULL;
}

static inline Cmd GetCxxCmd()
{
    char const* ccache = GetCCache();
    char const* cxx = GetCxx();

    if (ccache == NULL) {
        return Cmd(cxx);
    } else {
        return Cmd(ccache).Arg(cxx);
    }
}

// 构建 C++ 目标（库或者可执行程序）的环境。
struct CxxEnv {
    // 构建类型意味着一些默认的构建选项。
    enum BuildType {
        DEBUG,
        RELEASE,
        RELEASE_WITH_DEBUG_INFO,
    };

    // 链接类型决定目标是库时如何进行链接。
    //
    // - SHARED 表示进行动态链接。
    // - STATIC 表示进行静态链接。
    // - AUTO_LINK 表示默认为静态链接，但是可以被高层次的设置覆盖为动态链接。
    enum TargetLinkType {
        AUTO_LINK,
        SHARED,
        STATIC,
    };

    // 产物的根目录。
    Path build_dir { { "build" } };

    // 目标文件的根目录。
    Path obj_dir { { "obj" } };

    // 可执行文件的根目录。
    Path bin_dir { { "bin" } };

    // 动态库文件的根目录。
    Path lib_dir { { "lib" } };

    // 静态库文件的根目录。
    Path lib_static_dir { { "lib" } };

    BuildType build_type { DEBUG };
    TargetLinkType target_link_type { AUTO_LINK };

    // 额外的头文件搜索目录。
    std::vector<Path> include_dirs {};

    // 额外的 C++ 编译选项。
    std::vector<std::string> cxx_flags {};

    // 额外的链接选项。
    std::vector<std::string> ld_flags {};
};

Error PkgConfig(CxxEnv* env, std::string const& package);

static inline CxxEnv::TargetLinkType TargetOverride(CxxEnv::TargetLinkType default_type,
    CxxEnv::TargetLinkType target_type)
{
    switch (target_type) {
    case CxxEnv::SHARED:
    case CxxEnv::STATIC:
        return target_type;
    default:
        break;
    }

    switch (default_type) {
    case CxxEnv::AUTO_LINK:
        return CxxEnv::STATIC;
    default:
        break;
    }

    return default_type;
}

struct RebuildCheck {
    RebuildCheck(Error err, bool need_rebuild)
        : err(err)
        , need_rebuild(need_rebuild)
    {
    }

    Error err;
    bool need_rebuild;
};

static inline int64_t TimespecCmp(struct timespec& lhs, struct timespec& rhs)
{
    auto diff = (int64_t)lhs.tv_sec - (int64_t)rhs.tv_sec;
    if (diff != 0)
        return diff;
    return (int64_t)lhs.tv_nsec - (int64_t)rhs.tv_nsec;
}

class RebuildChecker {
public:
    RebuildChecker& Src(Path path)
    {
        src_.push_back(path);
        return *this;
    }

    RebuildChecker& Dst(Path path)
    {
        dst_.push_back(path);
        return *this;
    }

    RebuildCheck Check() const;

private:
    std::vector<Path> src_ {};
    std::vector<Path> dst_ {};
};

class CxxTarget {
public:
    enum TargetType {
        LIBRARY,
        EXECUTABLE,
    };

    CxxTarget(CxxEnv const* env, std::string name, TargetType target_type, CxxEnv::TargetLinkType target_link_type)
        : env_(env)
        , name_(std::move(name))
        , target_type_(target_type)
        , target_link_type_(TargetOverride(env_->target_link_type, target_link_type))
    {
    }

    // Add a source file into tree.
    CxxTarget& Src(Path src)
    {
        srcs_.emplace_back(std::move(src));
        return *this;
    }

    CxxTarget& Lib(Path lib)
    {
        libs_.emplace_back(std::move(lib));
        return *this;
    }

    CxxTarget& Dep(CxxTarget const& target)
    {
        if (!target.IsLibrary()) {
            NOSCRIPT_LOG(ERROR, "add a executable target (%s) as dependency, skip it", target.GetName().c_str());
            return *this;
        }

        libs_.emplace_back(target.GetTargetPath());
        return *this;
    }

    std::string const& GetName() const { return name_; }

    bool IsLibrary() const { return target_type_ == LIBRARY; }

    Path GetTargetPath() const
    {
        switch (target_type_) {
        case EXECUTABLE:
            return env_->build_dir / env_->bin_dir / name_;
        default:
            break;
        }

        switch (target_link_type_) {
        case CxxEnv::SHARED:
            return env_->build_dir / env_->lib_dir / (std::string("lib") + name_ + ".so");
        default:
            break;
        }

        return env_->build_dir / env_->lib_dir / (std::string("lib") + name_ + ".a");
    }

    Error Build()
    {
        if (srcs_.empty()) {
            NOSCRIPT_LOG(ERROR, "no source to target: %s", name_.c_str());
            return ERR_OOPS;
        }

        std::vector<Path> objs;
        Path path;

        objs.reserve(srcs_.size());
        for (auto const& src : srcs_) {
            NOSCRIPT_TRY(CxxCompile(src, &path));
            objs.emplace_back(std::move(path));
        }

        switch (target_type_) {
        case LIBRARY:
            if (target_link_type_ == CxxEnv::SHARED) {
                return CxxLinkShared(objs);
            } else {
                return CxxLinkStatic(objs);
            }
        case EXECUTABLE:
            return CxxLinkExecutable(objs);
        }
        return ERR_OK;
    }

private:
    Error CxxCompile(Path const& src, Path* obj_path)
    {
        auto cmd = GetCxxCmd().Arg("-c");

        for (auto const& each : env_->include_dirs) {
            cmd.Arg("-I").ArgPath(each);
        }

        for (auto const& each : env_->cxx_flags) {
            cmd.Arg(each);
        }

        if (target_link_type_ == CxxEnv::SHARED) {
            cmd.Arg("-fPIC");
        }

        if (env_->build_type == CxxEnv::RELEASE_WITH_DEBUG_INFO || env_->build_type == CxxEnv::RELEASE) {
            cmd.Arg("-O2");
        }

        if (env_->build_type == CxxEnv::RELEASE_WITH_DEBUG_INFO || env_->build_type == CxxEnv::DEBUG) {
            cmd.Arg("-g");
        }

        *obj_path = env_->build_dir / env_->obj_dir / src.SetExtension(".o");
        obj_path->Parent().Mkdir();

        return cmd.Arg("-o").ArgPath(*obj_path).ArgPath(src).Run();
    }

    Error CxxLinkExecutable(std::vector<Path> const& objs)
    {
        auto target = GetTargetPath();
        target.Parent().Mkdir();

        auto cmd = noscript::GetCxxCmd();

        cmd.Arg("-o").ArgPath(target);

        for (auto const& each : env_->ld_flags) {
            cmd.Arg(each);
        }

        for (auto const& each : objs) {
            cmd.ArgPath(each);
        }

        for (auto const& each : libs_) {
            cmd.ArgPath(each);
        }

        return cmd.Run();
    }

    Error CxxLinkStatic(std::vector<Path> const& objs)
    {
        auto target = GetTargetPath();
        target.Parent().Mkdir();

        auto cmd = noscript::Cmd("ar");
        cmd.Arg("rs").ArgPath(target);

        for (auto const& each : objs) {
            cmd.ArgPath(each);
        }

        for (auto const& each : libs_) {
            cmd.ArgPath(each);
        }

        return cmd.Run();
    }

    Error CxxLinkShared(std::vector<Path> const& objs)
    {
        auto target = GetTargetPath();
        target.Parent().Mkdir();

        auto cmd = noscript::GetCxxCmd();

        cmd.Arg("-shared").Arg("-o").ArgPath(target);

        for (auto const& each : env_->ld_flags) {
            cmd.Arg(each);
        }

        for (auto const& each : objs) {
            cmd.ArgPath(each);
        }

        for (auto const& each : libs_) {
            cmd.ArgPath(each);
        }

        return cmd.Run();
    }

    CxxEnv const* env_;
    std::string name_;
    TargetType target_type_;
    CxxEnv::TargetLinkType target_link_type_;
    std::vector<Path> srcs_ {};
    std::vector<Path> libs_ {};
};

// ----------------------------------------------------------------------------
// Rebuild
// ----------------------------------------------------------------------------

Error RebuildNoScript(int argc, char** argv);

} // namespace noscript

#endif
